package dev.blachut.svelte.lang.codeInsight

import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.testFramework.UsefulTestCase
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import dev.blachut.svelte.lang.SvelteTestScenario
import dev.blachut.svelte.lang.configureBundledSvelte
import dev.blachut.svelte.lang.doTestWithLangFromTestNameSuffix
import dev.blachut.svelte.lang.getSvelteTestDataPath

class SvelteCompletionTest : BasePlatformTestCase() {
  override fun getTestDataPath(): String = getSvelteTestDataPath()

  private val props = "\$props" // to trick Kotlin

  fun testScriptKeywordsJS() {
    myFixture.configureByText("Component.svelte", "<script><caret></script>")
    val items = myFixture.completeBasic()
    hasElements(items, "export")
    noElements(items, "interface")
  }

  fun testScriptKeywordsTS() {
    myFixture.configureByText("Component.svelte", """<script lang="ts"><caret></script>""")
    hasElements(myFixture.completeBasic(), "export", "interface")
  }

  fun testContextModuleAttribute() {
    myFixture.configureByText("foo.svelte", "<script c<caret>></script>")
    hasElements(myFixture.completeBasic(), """context="module"""")
  }

  fun testExistContextAttribute() {
    myFixture.configureByText("foo.svelte", """<script context="module" c<caret>></script>""")
    noElements(myFixture.completeBasic(), """context="module"""")
  }

  fun testStyleLang() {
    myFixture.configureByText("foo.svelte", """<style lang="<caret>"></style>""")
    hasElements(myFixture.completeBasic(), "css")
  }

  fun testSimpleTagAttribute() {
    myFixture.configureByText("foo.svelte", "<div c<caret>></div>")
    noElements(myFixture.completeBasic(), """context="module"""")
  }

  fun testSimpleTag() {
    myFixture.configureByText("Hello.svelte", "<h1>Test</h1>")
    myFixture.configureByText("foo.svelte", "<<caret>")
    hasElements(myFixture.completeBasic(), "div", "main", "svelte:body", "svelte:self", "svelte:element")
  }

  fun testSimpleTagNested() {
    myFixture.configureByText("Hello.svelte", "<h1>Test</h1>")
    myFixture.configureByText("foo.svelte", "<div><<caret></div>")
    hasElements(myFixture.completeBasic(), "div", "svelte:body", "svelte:self", "svelte:element")
  }

  fun testSimpleSvelteNamespace() {
    myFixture.configureByText("Hello.svelte", "<h1>Test</h1>")
    myFixture.configureByText("foo.svelte", "<svelte:<caret>")
    hasElements(myFixture.completeBasic(), "body", "self", "element")
  }

  fun testComponentImportNoScript() {
    myFixture.configureByText("Hello.svelte", "<h1>Test</h1>")
    myFixture.configureByText("Usage.svelte", """
      <h1><Hel<caret></h1>
    """.trimIndent())
    myFixture.completeBasic()
    myFixture.checkResult("""
      <script>
          import Hello from "./Hello.svelte";
      </script>

      <h1><Hello</h1>
    """.trimIndent())
  }

  fun testComponentImportWithScript() {
    myFixture.configureByText("Hello.svelte", "<h1>Test</h1>")
    myFixture.configureByText("Usage.svelte", """
      <h1><Hel<caret></h1>
      <script>
        console.log("hello")
      </script>
    """.trimIndent())
    myFixture.completeBasic()
    myFixture.checkResult("""
      <h1><Hello</h1>
      <script>
        import Hello from "./Hello.svelte";

        console.log("hello")
      </script>
    """.trimIndent())
  }

  fun testComponentImportSyncTagEditing() {
    myFixture.configureByText("Hello.svelte", "<h1>Test</h1>")
    myFixture.configureByText("Usage.svelte", """
      <h1><Hel<caret>></Hel></h1>
    """.trimIndent())
    myFixture.completeBasic()
    myFixture.checkResult("""
      <script>
          import Hello from "./Hello.svelte";
      </script>

      <h1><Hello></Hello></h1>
    """.trimIndent())
  }

  fun testComponentsTopLevel() {
    myFixture.configureByText("Hello1.svelte", "<h1>Test</h1>")
    myFixture.configureByText("Hello2.svelte", "<h1>Test</h1>")
    myFixture.configureByText("Usage.svelte", "<Hel<caret>")
    hasElements(myFixture.completeBasic(), "Hello1", "Hello2")
  }

  fun testComponentCaseSensitivity() {
    myFixture.configureByText("Hello1.svelte", "<h1>Test</h1>")
    myFixture.configureByText("Hello2.svelte", "<h1>Test</h1>")
    myFixture.configureByText("Usage.svelte", "<hel<caret>")
    hasElements(myFixture.completeBasic(), "Hello1", "Hello2")
  }

  fun testComponentImportVariants() {
    myFixture.addFileToProject("Hello1.svelte", "<h1>Test</h1>")
    myFixture.addFileToProject("lib.ts", """
      class SvelteComponent {} // Svelte 4+
      class Intermediate extends SvelteComponent {}
      class SvelteComponentTyped {} // Svelte 3
      
      export default class Hello3 extends SvelteComponent {}
      export class Hello4 extends SvelteComponent {}
      export class Hello5 extends Intermediate {}
      export class Hello6 extends SvelteComponentTyped {}
      export class HelloInvalid {}
    """.trimIndent())
    myFixture.configureByText("Usage.svelte", """
      <<caret>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "Hello1", "Hello3", "Hello4", "Hello5", "Hello6")
    noElements(items, "HelloInvalid", "SvelteComponent", "SvelteComponentTyped")
  }

  fun testComponentProps() {
    myFixture.configureByText("Hello1.svelte", """
    <script>
      export let hello11;
      let hello11NotAvailable = 10;
    </script>
    """.trimIndent())
    myFixture.configureByText("Hello2.svelte", """
    <script>
      export let hello22;
      let hello22NotAvailable = 10;
    </script>
    """.trimIndent())

    myFixture.configureByText("Usage.svelte", """
      <script>
        import Hello1 from "./Hello1.svelte";
      </script>
      <Hello1 <caret>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "slot", "hello11")
    noElements(items, "hello11NotAvailable", "hello22NotAvailable", "hello22")
  }

  fun testAfterDollarJS() = doTestWithLangFromTestNameSuffix(storeAfterDollar)

  fun testAfterDollarTS() = doTestWithLangFromTestNameSuffix(storeAfterDollar)

  private val storeAfterDollar = SvelteTestScenario { langExt, _ ->
    myFixture.configureBundledSvelte()
    myFixture.configureByText("stores.$langExt", """
      import { writable } from "svelte/store";
      export const count = writable(5);
    """.trimIndent())
    val count = "\$count" // to trick Kotlin
    myFixture.configureByText("Foo.svelte", """
      <script lang="$langExt">
        import { count } from "./stores";
      </script>

      <section>{$<caret>}</section>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "count")
    @Suppress("RemoveSingleExpressionStringTemplate")
    noElements(items, "$count")
  }

  fun testWithoutDollarJS() = doTestWithLangFromTestNameSuffix(storeWithoutDollar)

  fun testWithoutDollarTS() = doTestWithLangFromTestNameSuffix(storeWithoutDollar)

  private val storeWithoutDollar = SvelteTestScenario { langExt, _ ->
    myFixture.configureBundledSvelte()
    myFixture.configureByText("stores.$langExt", """
      import { writable } from "svelte/store";
      export const count = writable(5);
    """.trimIndent())
    val count = "\$count" // to trick Kotlin
    myFixture.configureByText("Foo.svelte", """
      <script lang="$langExt">
        import { count } from "./stores";
      </script>

      <section>{<caret>}</section>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "count")
    @Suppress("RemoveSingleExpressionStringTemplate")
    noElements(items, "$count")
  }

  fun testReactiveStatement() {
    myFixture.configureByText("Test.svelte", """
      <script>
        export let hello11 = "src"
        let hello11Local = "src";
        $: hello11Reactive = 20;

        $: true && hello<caret>
      </script>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "hello11", "hello11Local", "hello11Reactive")
    UsefulTestCase.assertSize(3, items)
  }

  fun testInterpolation() {
    myFixture.configureByText("Test.svelte", """
      <script>
        export let hello11;
        let hello11Local = 10;
        $: hello11Reactive = 20;
      </script>
      <div>{h<caret>}</div>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "hello11", "hello11Local", "hello11Reactive")
  }

  fun testInterpolationInAttribute() {
    myFixture.configureByText("Test.svelte", """
      <script>
        export let hello11 = "src"
        let hello11Local = "src";
        $: hello11Reactive = 20;
      </script>
      <img src={hello<caret>}>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "hello11", "hello11Local", "hello11Reactive")
    UsefulTestCase.assertSize(3, items)
  }

  fun testKeywords() {
    myFixture.configureByText("Test.svelte", """
      <div>{<caret>}</div>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "#if", "#each", "#await", "#snippet", "@html", "@debug", "@render", "@const")
  }

  fun testKeywords2() {
    myFixture.configureByText("Test.svelte", """
      <div>{#aw<caret>}</div>
    """.trimIndent())
    val items = myFixture.completeBasic()
    assertNull(items)
    myFixture.checkResult("""
      <div>{#await <caret>}</div>
    """.trimIndent())
  }

  fun testConstTagVariables() {
    myFixture.configureByText("Test.svelte", """
      {#if true}
        {@const localOne = 3}
        {<caret>}
        {@const localTwo = 3}
      {/if}
    """.trimIndent())
    hasElements(myFixture.completeBasic(), "localOne", "localTwo")
  }

  fun testMustache() {
    myFixture.configureByText("Hello.svelte", """
      <script></script>
      {#if <caret>}
      {/if}
    """.trimIndent())
    hasElements(myFixture.completeBasic(), "true", "false")
  }

  fun testSnippetParameterUsage() {
    myFixture.configureByText("Hello.svelte", """
      {#snippet figure(one, two)}
        <img src={<caret>} alt="image" />
      {/snippet}
    """.trimIndent())
    hasElements(myFixture.completeBasic(), "one", "two")
  }

  fun testSnippetRender() {
    myFixture.configureByText("Hello.svelte", """
      <script lang="ts">
        import type { Snippet } from 'svelte';
      
        let { children }: { children: Snippet } = $props();
      </script>
      
      {#snippet figure(image)}
        <img src={image.src} alt={image.caption} />
      {/snippet}
      
      <button>
        {@render <caret>}
      </button>
    """.trimIndent())
    hasElements(myFixture.completeBasic(), "children", "figure")
  }

  fun testSnippetFromScript() {
    myFixture.configureByText("Hello.svelte", """
      <script lang="ts">
        import type { Snippet } from 'svelte';
      
        let { children }: { children: Snippet } = $props();
        
        <caret>
      </script>
      
      {#snippet figure(image)}
        <img src={image.src} alt={image.caption} />
      {/snippet}
    """.trimIndent())
    hasElements(myFixture.completeBasic(), "children", "figure")
  }

  fun testDirectivesOnElement() {
    myFixture.configureByText("Usage.svelte", "<h1 <caret>>Test</h1>")
    val items = myFixture.completeBasic()
    hasElements(items, "bind:", "on:", "class:", "use:", "transition:", "in:", "out:", "animate:", "let:")
  }

  fun testDirectivesOnComponent() {
    myFixture.configureByText("Usage.svelte", "<Box <caret>>Test</Box>")
    val items = myFixture.completeBasic()
    hasElements(items, "bind:", "on:", "let:")
    noElements(items, "class:", "use:", "transition:", "in:", "out:", "animate:")
  }

  fun testBindDirective() {
    myFixture.configureByText("Usage.svelte", """
      <script>
        let localVariable = "localVariableValue"
      </script>
      <h1 bind:<caret>>Hello</h1>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "title")
    noElements(items, "onclick", "localVariable")
  }

  fun testOnDirective() {
    myFixture.configureByText("Usage.svelte", """
      <script>
        let localVariable = "localVariableValue"
      </script>
      <h1 on:<caret>>Hello</h1>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "click")
    noElements(items, "onclick", "localVariable")
  }

  fun testClassDirective() {
    myFixture.configureByText("Usage.svelte", """
      <script>
        let localVariable = "localVariableValue"
      </script>
      <h1 class:<caret>>Hello</h1>
      <style>
        .localClass {}
      </style>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "localVariable", "localClass")
  }

  fun testUseDirective() {
    myFixture.configureByText("Usage.svelte", """
      <script>
        function action() {}
      </script>
      <h1 use:<caret>>Hello</h1>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "action")
  }

  fun testTransitionDirective() {
    myFixture.configureByText("Usage.svelte", """
      <script>
        function fade() {}
      </script>
      <h1 transition:<caret>>Hello</h1>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "fade")
  }

  fun testInDirective() {
    myFixture.configureByText("Usage.svelte", """
      <script>
        function fade() {}
      </script>
      <h1 in:<caret>>Hello</h1>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "fade")
  }

  fun testOutDirective() {
    myFixture.configureByText("Usage.svelte", """
      <script>
        function fade() {}
      </script>
      <h1 out:<caret>>Hello</h1>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "fade")
  }

  fun testAnimateDirective() {
    myFixture.configureByText("Usage.svelte", """
      <script>
        function fade() {}
      </script>
      <h1 animate:<caret>>Hello</h1>
    """.trimIndent())
    val items = myFixture.completeBasic()
    hasElements(items, "fade")
  }

  fun testPrefetchAttribute() {
    myFixture.configureByText("Test.svelte", """
      <a svp<caret>>Hello</a>
    """.trimIndent())
    myFixture.completeBasic()
    myFixture.checkResult("""
      <a sveltekit:prefetch<caret>>Hello</a>
    """.trimIndent())
  }

  private fun checkElements(items: Array<LookupElement>, expected: Boolean, vararg variants: String) {
    val toCheck = setOf(*variants)
    val matched = mutableSetOf<String>()
    for (e in items) {
      val lookupString = e.lookupString
      if (toCheck.contains(lookupString)) {
        matched.add(lookupString)
      }
      if (matched.size == variants.size) break
    }
    if (expected) {
      UsefulTestCase.assertTrue("Actual: ${items.map { it.lookupString }}", matched.size == variants.size)
    }
    else {
      UsefulTestCase.assertTrue("Actual: ${items.map { it.lookupString }}", matched.isEmpty())
    }
  }

  private fun hasElements(items: Array<LookupElement>, vararg variants: String) {
    checkElements(items, true, *variants)
  }

  private fun noElements(items: Array<LookupElement>, vararg variants: String) {
    checkElements(items, false, *variants)
  }
}
